/*******************************************************************************
 * Copyright (c) 2012, 2014 Ericsson and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Marc Khouzam (Ericsson) - initial API and implementation
 *     Marc Dumais (Ericsson) - Bug 400231
 *     Marc Dumais (Ericsson) - Bug 399419
 *     Marc Dumais (Ericsson) - Bug 405390
 *     Marc Dumais (Ericsson) - Bug 396269
 *     Marc Dumais (Ericsson) - Bug 409512
 *     Marc Dumais (Ericsson) - Bug 409965
 *     Marc Dumais (Ericsson) - Bug 416524
 *     Xavier Raynaud (Kalray) - Bug 431935
 *******************************************************************************/

package org.eclipse.cdt.dsf.gdb.multicorevisualizer.internal.ui.view;

import java.util.List;

import org.eclipse.cdt.dsf.concurrent.ImmediateDataRequestMonitor;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.DataModelInitializedEvent;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IThreadDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IThreadDMData;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExitedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IResumedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IStartedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.ISuspendedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.StateChangeReason;
import org.eclipse.cdt.dsf.debug.service.IStack;
import org.eclipse.cdt.dsf.debug.service.IStack.IFrameDMContext;
import org.eclipse.cdt.dsf.debug.service.IStack.IFrameDMData;
import org.eclipse.cdt.dsf.gdb.multicorevisualizer.internal.ui.MulticoreVisualizerUIPlugin;
import org.eclipse.cdt.dsf.gdb.multicorevisualizer.internal.ui.model.VisualizerCore;
import org.eclipse.cdt.dsf.gdb.multicorevisualizer.internal.ui.model.VisualizerExecutionState;
import org.eclipse.cdt.dsf.gdb.multicorevisualizer.internal.ui.model.VisualizerModel;
import org.eclipse.cdt.dsf.gdb.multicorevisualizer.internal.ui.model.VisualizerThread;
import org.eclipse.cdt.dsf.gdb.multicorevisualizer.internal.utils.DSFDebugModel;
import org.eclipse.cdt.dsf.gdb.service.IGDBProcesses.IGdbThreadDMData;
import org.eclipse.cdt.dsf.mi.service.IMIExecutionDMContext;
import org.eclipse.cdt.dsf.mi.service.IMIProcessDMContext;
import org.eclipse.cdt.dsf.mi.service.IMIRunControl;
import org.eclipse.cdt.dsf.mi.service.IMIRunControl.MIRunMode;
import org.eclipse.cdt.dsf.mi.service.command.events.IMIDMEvent;
import org.eclipse.cdt.dsf.mi.service.command.events.MISignalEvent;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;


/**
 * DSF event listener class for the Multicore Visualizer.
 * This class will handle different relevant DSF events
 * and update the Multicore Visualizer accordingly.
 */
public class MulticoreVisualizerEventListener {
	
	private static final String THE_THREAD_ID_DOES_NOT_CONVERT_TO_AN_INTEGER = "The thread id does not convert to an integer: "; //$NON-NLS-1$
    // --- members ---

	/** Visualizer we're managing events for. */
	protected MulticoreVisualizer fVisualizer;

	
	// --- constructors/destructors ---
	
	/** Constructor */
	public MulticoreVisualizerEventListener(MulticoreVisualizer visualizer) {
		fVisualizer = visualizer;
	}

	
	// --- event handlers ---
	
	/** 
	 * Invoked when a thread or process is suspended. 
	 * Updates both state of the thread and the core it's running on
	 */
	@DsfServiceEventHandler
	public void handleEvent(final ISuspendedDMEvent event) {
		// make sure model exists
		final VisualizerModel model = fVisualizer.getModel();
		if (model == null) {
			return;
		}

		IDMContext context = event.getDMContext();
		

		// all-stop mode? If so, we take the opportunity, now that GDB has suspended
		// execution, to re-create the model so that we synchronize with the debug session
		if (context != null && isSessionAllStop(context.getSessionId()) ) {
			fVisualizer.update();
			return;
		}
		
		// non-stop mode
		if (context instanceof IContainerDMContext) {
    		// We don't deal with processes
    	} else if (context instanceof IMIExecutionDMContext) {
    		// Thread suspended
    		
    		final IMIExecutionDMContext execDmc = (IMIExecutionDMContext)context;
			IThreadDMContext threadContext =
					DMContexts.getAncestorOfType(execDmc, IThreadDMContext.class);

			final DsfServicesTracker tracker =
					new DsfServicesTracker(MulticoreVisualizerUIPlugin.getBundleContext(), 
                                           execDmc.getSessionId());
			IProcesses procService = tracker.getService(IProcesses.class);
			final IStack stackService = tracker.getService(IStack.class);
			tracker.dispose();
			
			procService.getExecutionData(threadContext, 
				new ImmediateDataRequestMonitor<IThreadDMData>() {
					@Override
					protected void handleSuccess() {
						IThreadDMData data = getData();
					
						// Check whether we know about cores
						if (data instanceof IGdbThreadDMData) {
							String[] cores = ((IGdbThreadDMData)data).getCores();
							if (cores != null) {
								assert cores.length == 1; // A thread belongs to a single core
								int coreId = Integer.parseInt(cores[0]);
								final VisualizerCore vCore = model.getCore(coreId);
								
						        int tid;
						        VisualizerThread threadTmp = null;
						        try {
						            tid = Integer.parseInt(execDmc.getThreadId());
	                                threadTmp = model.getThread(tid);
						        } catch (NumberFormatException e) {
						            // unable to resolve thread
						            assert false : THE_THREAD_ID_DOES_NOT_CONVERT_TO_AN_INTEGER + execDmc.getThreadId();
						            return;
						        }
					    		
					    		if (threadTmp != null) {
					    		    final VisualizerThread thread = threadTmp;
					    			assert thread.getState() == VisualizerExecutionState.RUNNING;
					    			
									VisualizerExecutionState _newState = VisualizerExecutionState.SUSPENDED;

					    			if (event.getReason() == StateChangeReason.SIGNAL) {
					    				if (event instanceof IMIDMEvent) {
					    					Object miEvent = ((IMIDMEvent)event).getMIEvent();
					    					if (miEvent instanceof MISignalEvent) {
					    						String signalName = ((MISignalEvent)miEvent).getName();
					    						if (DSFDebugModel.isCrashSignal(signalName)) {
													_newState = VisualizerExecutionState.CRASHED;
					    						}
					    					}
					    				}
					    			}
									final VisualizerExecutionState newState = _newState;
									if (stackService != null) {
										stackService.getTopFrame(execDmc,
												new ImmediateDataRequestMonitor<IFrameDMContext>(null) {
													@Override
													protected void handleCompleted() {
														IFrameDMContext targetFrameContext = null;
														if (isSuccess()) {
															targetFrameContext = getData();
														}
														if (targetFrameContext != null) {
															stackService.getFrameData(targetFrameContext,
																new ImmediateDataRequestMonitor<IFrameDMData>(null) {
																	@Override
																	protected void handleCompleted() {
																		IFrameDMData frameData = null;
																		if (isSuccess()) {
																			frameData = getData();
																		}
																		updateThread(thread, newState, vCore, frameData);
																	}
																});
														} else {
															updateThread(thread, newState, vCore, null);
														}
													}
											});
									} else {
										updateThread(thread, newState, vCore, null);
									}
									
					    		}
							}
						}
					}
				}
			);
    	}
	}
	
	private void updateThread(VisualizerThread thread, VisualizerExecutionState newState, VisualizerCore vCore, IFrameDMData frameData) {
		thread.setState(newState);
		thread.setCore(vCore);
		thread.setLocationInfo(frameData);
		fVisualizer.refresh();
	}

	/** Invoked when a thread or process is resumed. */
	@DsfServiceEventHandler
	public void handleEvent(IResumedDMEvent event) {
		// make sure model exists
		VisualizerModel model = fVisualizer.getModel();
		if (model == null) {
			return;
		}
		
		IDMContext context = event.getDMContext();
		
		// in all-stop mode... : update all threads states to "running"
		if (context != null && isSessionAllStop(context.getSessionId()) ) {
			List<VisualizerThread> tList = model.getThreads();
			for(VisualizerThread t : tList) {
				t.setState(VisualizerExecutionState.RUNNING);
				t.setLocationInfo((String) null);
			}
			fVisualizer.getMulticoreVisualizerCanvas().requestUpdate();
			return;
		}
		
		// Non-stop mode
		if (context instanceof IContainerDMContext) {
    		// We don't deal with processes
    	} else if (context instanceof IMIExecutionDMContext) {
    		// Thread resumed
            int tid;
            VisualizerThread thread = null;
            String strThreadId = ((IMIExecutionDMContext) context).getThreadId();
            try {
                tid = Integer.parseInt(strThreadId);
                thread = model.getThread(tid);
            } catch (NumberFormatException e) {
                // unable to resolve thread
                assert false : THE_THREAD_ID_DOES_NOT_CONVERT_TO_AN_INTEGER + strThreadId;
                return;
            }

            if (thread != null) {
    			assert thread.getState() == VisualizerExecutionState.SUSPENDED ||
     				   thread.getState() == VisualizerExecutionState.CRASHED;
    			
    			thread.setState(VisualizerExecutionState.RUNNING);
    			thread.setLocationInfo((String) null);
    			fVisualizer.getMulticoreVisualizerCanvas().requestUpdate();
    		}
    	}
	}

	/** Invoked when a thread or process starts. */
	@DsfServiceEventHandler
	public void handleEvent(IStartedDMEvent event) {
		// make sure model exists
		final VisualizerModel model = fVisualizer.getModel();
		if (model == null) {
			return;
		}
		
		IDMContext context = event.getDMContext();
		if (context == null) return;
		final String sessionId = context.getSessionId();
		
		// all-stop mode? 
		// If so we can't ask GDB for more info about the new thread at this moment.  
		// So we still add it to the model, on core zero and with a OS thread id of 
		// zero.  The next time the execution is stopped, the model will be re-created 
		// and show the correct thread ids and cores.
		if (isSessionAllStop(sessionId) && context instanceof IMIExecutionDMContext ) {
			final IMIExecutionDMContext execDmc = (IMIExecutionDMContext)context;
			final IMIProcessDMContext processContext =
					DMContexts.getAncestorOfType(execDmc, IMIProcessDMContext.class);
			
			// put it on core zero
			VisualizerCore vCore = model.getCore(0);
			if (vCore == null) return;
			
			int pid = Integer.parseInt(processContext.getProcId());

            int tid;
            try {
                tid = Integer.parseInt(execDmc.getThreadId());
            } catch (NumberFormatException e) {
                // unable to resolve thread
                assert false : THE_THREAD_ID_DOES_NOT_CONVERT_TO_AN_INTEGER + execDmc.getThreadId();
                return;
            }

			int osTid = 0;

			// add thread if not already there - there is a potential race condition where a 
			// thread can be added twice to the model: once at model creation and once more 
			// through the listener.   Checking at both places to prevent this.
			if (model.getThread(tid) == null ) {
				model.addThread(new VisualizerThread(vCore, pid, osTid, tid, VisualizerExecutionState.RUNNING));
				fVisualizer.getMulticoreVisualizerCanvas().requestUpdate();	
			}
			return;
		}
		
		// non-stop mode
		if (context instanceof IContainerDMContext) {
    		// We don't deal with processes
    	} else if (context instanceof IMIExecutionDMContext) {
    		// New thread added
    		final IMIExecutionDMContext execDmc = (IMIExecutionDMContext)context;
			final IMIProcessDMContext processContext =
					DMContexts.getAncestorOfType(execDmc, IMIProcessDMContext.class);
			IThreadDMContext threadContext =
					DMContexts.getAncestorOfType(execDmc, IThreadDMContext.class);

			DsfServicesTracker tracker = 
					new DsfServicesTracker(MulticoreVisualizerUIPlugin.getBundleContext(), 
							sessionId);
			IProcesses procService = tracker.getService(IProcesses.class);
			tracker.dispose();
			
			procService.getExecutionData(threadContext, 
				new ImmediateDataRequestMonitor<IThreadDMData>() {
					@Override
					protected void handleSuccess() {
						IThreadDMData data = getData();
					
						// Check whether we know about cores
						if (data instanceof IGdbThreadDMData) {
							String[] cores = ((IGdbThreadDMData)data).getCores();
							if (cores != null) {
								assert cores.length == 1; // A thread belongs to a single core
								int coreId = Integer.parseInt(cores[0]);
								VisualizerCore vCore = model.getCore(coreId);
								// There is a race condition that sometimes happens here.  We can reach
                                // here because we were notified that a thread is started, but the model
								// is not yet completely constructed.  If the model doesn't yet contain the
								// core the thread runs-on, the getCore() call above will return null.  This
								// will later cause a problem when we try to draw this thread, if we allow
								// this to pass.  See Bug 396269/
                                if (vCore == null)
                                    return;
								
								int pid = Integer.parseInt(processContext.getProcId());
                                int tid;
                                try {
                                    tid = Integer.parseInt(execDmc.getThreadId());
                                } catch (NumberFormatException e) {
                                    // Unable to resolve thread information
                                    assert false : THE_THREAD_ID_DOES_NOT_CONVERT_TO_AN_INTEGER + execDmc.getThreadId();
                                    return;
                                }
								
								int osTid = 0;
								try {
									osTid = Integer.parseInt(data.getId());
								} catch (NumberFormatException e) {
									// I've seen a case at startup where GDB is not ready to
									// return the osTID so we get null.  
									// That is ok, we'll be refreshing right away at startup
								}

								// add thread if not already there - there is a potential race condition where a 
								// thread can be added twice to the model: once at model creation and once more 
								// through the listener.   Checking at both places to prevent this.
								if (model.getThread(tid) == null ) {
									model.addThread(new VisualizerThread(vCore, pid, osTid, tid, VisualizerExecutionState.RUNNING));
									fVisualizer.getMulticoreVisualizerCanvas().requestUpdate();	
								}
							}
						}
					}
				}
			);
    	}
	}
	
	/** Invoked when a thread or process exits. */
	@DsfServiceEventHandler
	public void handleEvent(IExitedDMEvent event) {
		// make sure model exists
		final VisualizerModel model = fVisualizer.getModel();
		if (model == null) {
			return;
		}

		IDMContext context = event.getDMContext();
		final MulticoreVisualizerCanvas canvas = fVisualizer.getMulticoreVisualizerCanvas();
		
		if (context instanceof IContainerDMContext) {
			// process exited 
			
			// Note: this is required because we noticed that in GDB 7.6 and older, 
			// the "thread exited" signal is not sent for the local detach case.  
			// see bug 409512
			DsfServicesTracker tracker = 
					new DsfServicesTracker(MulticoreVisualizerUIPlugin.getBundleContext(), 
                                           context.getSessionId());
			IProcesses procService = tracker.getService(IProcesses.class);
			tracker.dispose();
			
			// get all threads associated to this process and
			// mark them as exited in the model.
			procService.getProcessesBeingDebugged(context, 
					new ImmediateDataRequestMonitor<IDMContext[]>() {
						@Override
						protected void handleSuccess() {
							assert getData() != null;
							
							IDMContext[] contexts = getData();
							for (IDMContext c : contexts) {
								if (c instanceof IMIExecutionDMContext) {
                                    int tid;
                                    String strThreadId = ((IMIExecutionDMContext) c).getThreadId();
                                    try {
                                        tid = Integer.parseInt(strThreadId);
                                    } catch (NumberFormatException e) {
                                        // unable to resolve the thread id
                                        assert false : THE_THREAD_ID_DOES_NOT_CONVERT_TO_AN_INTEGER + strThreadId;
                                        continue;
                                    }

									model.markThreadExited(tid);
								}
							}
							
							if (canvas != null) {
								canvas.requestUpdate();
							}
						}
						@Override
						protected void handleFailure() {
							// we are overriding handleFailure() to avoid an error message
							// in the log, in the all-stop mode.
						}
			});
			

    	} else if (context instanceof IMIExecutionDMContext) {
    		// Thread exited
            int tid;
            String strThreadId = ((IMIExecutionDMContext) context).getThreadId();
            try {
                tid = Integer.parseInt(strThreadId);
                model.markThreadExited(tid);
            } catch (NumberFormatException e) {
                assert false : THE_THREAD_ID_DOES_NOT_CONVERT_TO_AN_INTEGER + strThreadId;
            }

			if (canvas != null) {
				canvas.requestUpdate();
			}
    	}
	}

	
	/** Invoked when the debug data model is ready */
	@DsfServiceEventHandler
	public void handleEvent(DataModelInitializedEvent event) {
		// re-create the visualizer model now that CPU and core info is available
		fVisualizer.update();
	}
	
	
	// helper functions

	/** Returns whether the session is the "all-stop" kind */
	private boolean isSessionAllStop(String sessionId) {
		DsfServicesTracker servicesTracker = new DsfServicesTracker(MulticoreVisualizerUIPlugin.getBundleContext(), sessionId);
		IMIRunControl runCtrlService = servicesTracker.getService(IMIRunControl.class);
		servicesTracker.dispose();
		
		if (runCtrlService != null && runCtrlService.getRunMode() == MIRunMode.ALL_STOP ) {
			return true;
		}
		return false;
	}
}

